/**
 * Copyright (c) 2011 Doug Wightman, Zi Ye
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
*/

package org.eclipse.recommenders.snipmatch.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

/**
 * This class represents a match "environment", which is responsible for getting code context information
 * and integrating the returned matches. User should extend this class.
 */
public abstract class MatchEnvironment {

	private HashMap<ArgumentMatchNode, String[]> cachedArgumentCompletions;
	
	public MatchEnvironment() {
		
		cachedArgumentCompletions = new HashMap<ArgumentMatchNode, String[]>();
	}
	
	/**
	 * Returns the name of this environment.
	 * @return The name of this environment.
	 */
	public abstract String getName();
	
	/**
	 * Returns the friendly name of this environment.
	 * @return The friendly name of this environment.
	 */
	public abstract String getFriendlyName();
	
	/**
	 * Returns an array of major types handled by this environment.
	 * @return An array of major types handled by this environment.
	 */
	public abstract String[] getMajorTypes();

	/**
	 * Returns n array of the friendly names of the major types handled by this environment.
	 * @return An array of the friendly names of the major types handled by this environment.
	 */
	public abstract String[] getFriendlyMajorTypes();
	
	/**
	 * Returns token interpretations for a search query.
	 * @param query The search query.
	 * @return A hash map containing the interpretations for each token in the query.
	 */
	public HashMap<String, String> getQueryTokenInterpretations(String query) {
		
		HashMap<String, String> interps = new HashMap<String, String>();
		String[] tokens = query.split(" ");
		
		for (String token : tokens) {
			interps.put(token, getQueryTokenInterpretation(token));
		}
		
		return interps;
	}
	
	/**
	 * Tests a match returned by the search to see if it is valid within the current environment.
	 * @param match The match returned by the search.
	 * @return True if the match is valid, false otherwise.
	 */
	public abstract boolean testMatch(MatchNode match);
	
	/**
	 * Reset the match environment to its default state. For example, all code changes are rolled back.
	 */
	public abstract void reset();
	
	/**
	 * Applies a match to the environment.
	 * @param match The match to apply.
	 * @throws Exception
	 */
	public void applyMatch(MatchNode match) throws Exception {
		
		// First, get the evaluated result of the match.
		Object result = evaluateMatchNode(match);
		
		// Apply the result of the match to the environment.
		if (match instanceof EffectMatchNode) {
			
			Effect effect = ((EffectMatchNode) match).getEffect();
			applyResult(result, effect);
		}
		else applyResult(result, null);
	}
	
	/**
	 * Returns an array of matches generated by completing the given match's tokens.
	 * @param match The incomplete match.
	 * @return An array of matches generated by completing the given match's tokens.
	 */
	public MatchNode[] getMatchCompletions(MatchNode match) {

		ArrayList<MatchNode> matchCompletions = new ArrayList<MatchNode>();

		HashMap<ArgumentMatchNode, String[]> tokenCompletions = new HashMap<ArgumentMatchNode, String[]>();

		// Collect an array of completions for each argument token in the match.
		collectArgumentCompletions(match, tokenCompletions);
		
		int numCompletions = 1;
		
		Set<ArgumentMatchNode> keys = tokenCompletions.keySet();

		// Calculate the number of possible combinations of token completions.
		for (ArgumentMatchNode argNode : keys) {
			numCompletions *= tokenCompletions.get(argNode).length;
		}
		
		// The indices indicating which completion should be used for each token.
		HashMap<ArgumentMatchNode, Integer> completionIndices = new HashMap<ArgumentMatchNode, Integer>();

		for (ArgumentMatchNode argNode : keys) {
			completionIndices.put(argNode, 0);
		}
		
		// For each possible combination of token completions...
		for (int i = 0; i < numCompletions; i++) {
			
			HashMap<ArgumentMatchNode, String> substitutions = new HashMap<ArgumentMatchNode, String>();

			// Generate the substitutions for all argument tokens based on the current indices.
			for (ArgumentMatchNode argNode : keys) {
				substitutions.put(argNode, tokenCompletions.get(argNode)[completionIndices.get(argNode)]);
			}

			// Add the match that is formed by performing the substitutions.
			matchCompletions.add(completeMatch(match, substitutions));
			
			// Calculate the set of indices for the next combination of token completions.
			for (ArgumentMatchNode argNode : keys) {
				
				Integer index = completionIndices.get(argNode);
				index = (index + 1) % tokenCompletions.get(argNode).length;
				completionIndices.put(argNode, index);
			}
		}
		
		return matchCompletions.toArray(new MatchNode[matchCompletions.size()]);
	}
	
	/**
	 * Returns an array of completions for one argument. Must be implemented by derived classes.
	 * @param argNode The argument node to complete.
	 * @return An array of possible completions for the given argument.
	 */
	public abstract String[] getArgumentCompletions(ArgumentMatchNode argNode);
	
	/**
	 * Returns the best possible interpretation of the given token's type, in the current context.
	 * @param token The token to interpret.
	 * @return The full type string of the given token. e.g., expr:java.io.File
	 */
	protected abstract String getQueryTokenInterpretation(String token);

	/**
	 * Evaluate one match. The evaluated result of the root match node is applied to the code environment.
	 * @param node The match node to evaluate.
	 * @return The result of the evaluation.
	 */
	private Object evaluateMatchNode(MatchNode node) {
		
		/* For argument nodes, simply return the argument. 
		 * For effect nodes, use evaluateEffect and pass in the arguments.
		 */
		
		if (node instanceof EffectMatchNode) {
			
			EffectMatchNode effectNode = (EffectMatchNode) node;
	
			Object[] args = new Object[effectNode.getEffect().numParameters()];
			
			for (int i = 0; i < args.length; i++) {
				args[i] = evaluateMatchNode(effectNode.getChild(i));
			}
			
			return evaluateEffect(effectNode, args);
		}
		else return ((ArgumentMatchNode) node).getArgument();
	}
	
	/**
	 * Evaluates one effect with the given arguments. Must be implemented by derived classes.
	 * @param effectNode The effect to evaluate.
	 * @param args The arguments for the effect.
	 * @return The result of the evaluation.
	 */
	protected abstract Object evaluateEffect(EffectMatchNode effectNode, Object[] args);
	
	/**
	 * Apply the evaluated result of a match to the environment.
	 * @param result The evaluated result of the match.
	 * @param effect The effect of the root match node.
	 * @throws Exception
	 */
	protected abstract void applyResult(Object result, Effect effect) throws Exception;
	
	/**
	 * Collects token completions for a match.
	 * @param match The match for which to find token completions.
	 * @param completions The map of token completions to modify.
	 */
	private void collectArgumentCompletions(MatchNode match,
			HashMap<ArgumentMatchNode, String[]> completions) {
		
		if (match instanceof EffectMatchNode) {

			EffectMatchNode effectNode = (EffectMatchNode) match;
			
			/* The ghost child is used to make an entry in the completions for effects with no parameters.
			 * Because match completions are generated from token completions, the ghost child is a way to
			 * make sure that effects with no parameters are still considered when making match completions.
			 */
			if (effectNode.numChildren() == 0) {
				completions.put(effectNode.getGhostChild(), new String[] {null});
			}
	
			// Recursively collect argument completions for child nodes.
			for (MatchNode child : effectNode.getChildren()) {
				collectArgumentCompletions(child, completions);
			}
		}
		else {
			
			ArgumentMatchNode argNode = (ArgumentMatchNode) match;
			String[] argCompletions;
	
			if (cachedArgumentCompletions.containsKey(argNode)) {
				argCompletions = cachedArgumentCompletions.get(argNode);
			}
			else {
				argCompletions = getArgumentCompletions(argNode);
				cachedArgumentCompletions.put(argNode, argCompletions);
			}
			
			completions.put(argNode, argCompletions);
		}
	}
	
	/**
	 * Complete a match by making the given substitutions.
	 * @param match The match to complete.
	 * @param substitutions The substitutions to make.
	 * @return The completed match.
	 */
	private MatchNode completeMatch(MatchNode match,
			HashMap<ArgumentMatchNode, String> substitutions) {
		
		if (match instanceof EffectMatchNode) {
			
			EffectMatchNode oldMatch = (EffectMatchNode) match;
			ArrayList<MatchNode> newChildren = new ArrayList<MatchNode>();
			
			for (MatchNode child : ((EffectMatchNode) match).getChildren()) {
				
				MatchNode newChild = completeMatch(child, substitutions);
				newChildren.add(newChild);
			}
			
			EffectMatchNode newMatch = new EffectMatchNode(oldMatch.getEffect(),
					oldMatch.getPattern(), newChildren.toArray(new MatchNode[newChildren.size()]));
			
			newMatch.setMatchType(oldMatch.getMatchType());
			
			return newMatch;
		}
		else {
			
			ArgumentMatchNode oldMatch = (ArgumentMatchNode) match;
			ArgumentMatchNode newMatch = new ArgumentMatchNode(
					substitutions.get(oldMatch), oldMatch.getParameter());

			newMatch.setMatchType(oldMatch.getMatchType());
			
			return newMatch;
		}
	}
}
